﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Configuration;
using System.Globalization;

using BizElements.Core;

namespace BizElements.BusinessLayer
{
    #region Documentation.
    /// <summary>
    /// Text source that reads resources from database.
    /// </summary>
    /// <remarks>
    /// <para><b>DbTextSource</b> fetches and caches messages for a single application module from <b>TextSource</b> table.
    /// A single instance is used to fetch resources for all languages and cultures. When compared to standard .NET RESX 
    /// usage scenarios, this would imply that a single text source instance contains messages of default and culture specific 
    /// RESX files. Consider a global resource file “Messages.resx” which contains messages for default culture. In this example 
    /// we will use <b>en</b> as default culture. To localize your application module for US residents and German speakers you 
    /// would add the following RESX files: “Messages.en-US.resx” and “Messages.de.resx”. All these RESX files represent localized 
    /// versions of the same “Messages” module (global resource), thus, when using <b>DbTextSource</b> you would instantiate 
    /// an object as shown in the following example.
    /// <example><code>new DbTextSource("Messages");</code></example>
    /// </para>
    /// <para>Instead of using "Messages" name, it might be better to use a fully qualified class name such as 
    /// “MySolution.BusinesLogicLayer.Messages” and “MySolution.Web.Messages” for different assemblies and/or application layers.</para>
    /// <para>The implementation automatically remotes all data-access operations in accordance 
    /// with the configuration provided by <see cref="BizElements.BusinessLayer.RpcHelper"/>.</para>
    /// <para>Required application settings:
    /// <code>
    /// &lt;appSettings&gt;
    ///     &lt;!-- One of the supported DBMS types that can be used to store resources. --&gt;
    ///     &lt;add key="BizElements.BusinessLayer.DbTextSource.DbmsType" value="SqlServer_2000"/&gt;
    ///     &lt;!-- Application name/key. --&gt;
    ///     &lt;add key="BizElements.BusinessLayer.DbTextSource.Application" value="MyApplication"/&gt;
    ///     &lt;!-- Default culture name. --&gt;
    ///     &lt;add key="BizElements.BusinessLayer.DbTextSource.DefaultCulture" value="en-US"/&gt;    
    /// &lt;/appSettings&gt;
    /// &lt;connectionStrings&gt;
    ///     &lt;!-- Text resources/messages database. --&gt;
    ///     &lt;add name="BizElements.BusinessLayer.DbTextSource.ConnectionString" connectionString="data source=.;initial catalog=MyDatabase;UID=DbUser;PWD=BigSecret"/&gt;
    /// &lt;/connectionStrings&gt;
    /// </code>
    /// </para>
    /// <para>Optional settings/customization:
    /// <code>
    /// &lt;appSettings&gt;
    ///     &lt;!-- Number of minutes after which DbTextSource will automatically refresh cached item. --&gt;
    ///     &lt;!-- Choose higher value if messages are never changed, or a lower value if messages are frequently updated. --&gt;
    ///     &lt;!-- Default value is equal to GlobalCache.DefaultAbsoluteExpirationInMinutes. --&gt;
    ///     &lt;add key="BizElements.BusinessLayer.DbTextSource.CacheExpirationInMinutes" value="500"/&gt;
    ///     &lt;!-- Database table which contains the resources. Default is TextSource. --&gt;
    ///     &lt;add key="BizElements.BusinessLayer.DbTextSource.TableName" value="TextSource"/&gt;
    /// &lt;/appSettings&gt;
    /// </code>
    /// </para>
    /// </remarks>
    #endregion
    public class DbTextSource : ITextSource
    {
        #region Configuration.

        static readonly string Application;
        static readonly CultureInfo DefaultCulture;
        static readonly int CacheExpirationInMinutes;
        internal static readonly string TableName;

        static DbTextSource()
        {
            Application = ConfigurationManager.AppSettings["BizElements.BusinessLayer.DbTextSource.Application"];

            string cfgDefaultCulture = ConfigurationManager.AppSettings["BizElements.BusinessLayer.DbTextSource.DefaultCulture"];
            if (!string.IsNullOrEmpty(cfgDefaultCulture))
                DefaultCulture = new CultureInfo(cfgDefaultCulture);
            else
                DefaultCulture = CultureInfo.InvariantCulture;

            int? cfgCacheExpiration = NumberParser.ParseInt(ConfigurationManager.AppSettings["BizElements.BusinessLayer.DbTextSource.CacheExpirationInMinutes"]);
            if (cfgCacheExpiration.HasValue && cfgCacheExpiration.Value >= 0)
                CacheExpirationInMinutes = cfgCacheExpiration.Value;
            else
                CacheExpirationInMinutes = GlobalCache.DefaultAbsoluteExpirationInMinutes;

            TableName = TextSourceCatalog.GetTableNameOverride("TextSource") ?? "TextSource";
        }

        #endregion

        /// <summary>
        /// Initializes a new instance of <b>DbTextSource</b> class.
        /// </summary>
        /// <param name="module">Application module for which text is provided.</param>
        public DbTextSource(string module)
        {            
            if (string.IsNullOrEmpty(module))
                throw new ArgumentException("Application module must be specified.", "module");

            this.Module = module;
        }

        /// <summary>
        /// Application module for which text is provided.
        /// </summary>
        public string Module { get; private set; }

        /// <summary>
        /// Gets or sets optional secondary text source which is used when requested entries do not exist in current text source.
        /// </summary>
        public ITextSource ChainedTextSource { get; set; }

        #region ITextSource.

        CultureInfo currentCulture = DefaultCulture;

        /// <summary>
        /// Gets or sets the current culture.
        /// </summary>
        public CultureInfo CurrentCulture
        {
            get { return this.currentCulture; }
            set { this.currentCulture = value ?? DefaultCulture; }
        }

        /// <summary>
        /// Resets CurrentCulture property.
        /// </summary>
        public void UseDefaultCulture()
        {
            this.CurrentCulture = DefaultCulture;
        }

        /// <summary>
        /// Gets string with the specified key. Text is in current culture language.
        /// </summary>
        /// <param name="key">Unique key in a text source.</param>
        /// <returns>String that matches the given key. Null if it doesn't exist.</returns>
        public string GetString(string key)
        {
            return GetString(key, this.CurrentCulture);
        }

        /// <summary>
        /// Gets string with the given key written for the specified culture/language.
        /// </summary>
        /// <param name="key">Unique key in a text source.</param>
        /// <param name="culture">Specifies culture and language of the requested string.</param>
        /// <returns>String that matches the given key. Null if it doesn't exist.</returns>
        public string GetString(string key, CultureInfo culture)
        {
            if (culture == null)
                culture = DefaultCulture;

            string value = GetString(Application, this.Module, key, culture.TwoLetterISOLanguageName, culture.Name);
            if (value == null && this.ChainedTextSource != null)
                value = this.ChainedTextSource.GetString(key, culture);

            return value;
        }

        #endregion

        #region Message and keys/names retrieval - cache, data access. 
       
        /// <summary>
        /// Gets all resource keys for module specified in <see cref="Module"/> property.
        /// </summary>
        /// <returns>Collection of message names/keys.</returns>
        public IDictionary GetKeys()
        {
            ModuleResources moduleMessages = GetModuleResources(Application, this.Module);
            return moduleMessages.Keys;
        }

        private static string GetString(string application, string module, string name, string languageCode, string cultureCode)
        {
            ModuleResources moduleMessages = GetModuleResources(application, module);
            return moduleMessages.TryGetString(name, languageCode, cultureCode);
        }

        private static ModuleResources GetModuleResources(string application, string module)
        {
            string cacheKey = GetModuleCacheKey(application, module);
            ModuleResources moduleMessages = GlobalCache.GetData(cacheKey) as ModuleResources;
            if (moduleMessages == null)
            {
                var entities = DbTextSourceManager.FetchForModule(application, module);
                moduleMessages = new ModuleResources();
                moduleMessages.FromEntities(entities);
                GlobalCache.SetData(cacheKey, moduleMessages, CacheExpirationInMinutes, /*absolute expiration*/ false, GetModuleCacheCategories(application, module));
            }

            return moduleMessages;
        }

        private static string GetModuleCacheKey(string application, string module)
        {
            return string.Format("DbTextSource_{0}###{1}", application, module);
        }

        private static string[] GetModuleCacheCategories(string application, string module)
        {
            return new string[] { TableName };
        }

        #endregion

        private class ModuleResources
        {
            #region Messages with culture specific keys and a list of culture neutral keys/names.

            private IDictionary<string, string> messagesWithCultureSpecificKeys;
            public IDictionary Keys { get; private set; }

            public void FromEntities(EntityCollection<TextSourceEntity, TextSourceMeta> entities)
            {
                // In case of neutral cultures, ie. where cultureCode = languageCode, language keys are created.
                this.messagesWithCultureSpecificKeys = new SortedDictionary<string, string>();
                this.Keys = new SortedList(entities.Count);
                foreach (TextSourceEntity record in entities)
                {
                    this.messagesWithCultureSpecificKeys[GetMessageKeyForCulture(record.Name, record.CultureCode)] = record.Value;
                    this.Keys[record.Name] = null;
                }
            }

            public string TryGetString(string name, string languageCode, string cultureCode)
            {
                foreach (string messageKey in GetMessageKeysSortedByRelevance(name, languageCode, cultureCode))
                {
                    string value = null;
                    this.messagesWithCultureSpecificKeys.TryGetValue(messageKey, out value);
                    if (value != null)
                        return value;
                }

                return null;
            }

            private static string GetMessageKeyForLanguage(string name, string languageCode)
            {
                return string.Format("{0}###{1}", name, languageCode);
            }

            private static string GetMessageKeyForCulture(string name, string cultureCode)
            {
                // May be same as language key for neutral cultures.
                return string.Format("{0}###{1}", name, cultureCode);
            }

            private static string GetMessageKeyForDefaultCulture(string name)
            {
                //return string.Format("{0}###{1}", name, DefaultCulture.Name);
                return name;
            }

            private static string[] GetMessageKeysSortedByRelevance(string name, string languageCode, string cultureCode)
            {
                // Three attempts:
                //  - specified culture code,
                //  - neutral culture, ie. language code,
                //  - default culture code.
                return new string[3] 
            { 
                GetMessageKeyForCulture(name, cultureCode),
                GetMessageKeyForLanguage(name, languageCode),                
                GetMessageKeyForDefaultCulture(name)
            };
            }

            #endregion
        }
    }    
}
